use crate::error::*;
use crate::LOGGER;
use error_chain::bail;
use rand::Rng;
use slog::{error, info};
use std::fs::OpenOptions;
use std::io::{Read, Write};
use std::path::Path;
use std::thread;
use std::time::Duration;
use subprocess::{Popen, PopenConfig, Redirection};
use crate::STAT;

pub async fn compile<T: AsRef<Path>, E: AsRef<Path>>(source_path: T, target_path: E) -> Result<()> {
    info!(
        LOGGER,
        "Start to compile {}",
        source_path
            .as_ref()
            .to_str()
            .ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?
    );
    let mut handler = Popen::create(
        &[
            "g++",
            source_path
                .as_ref()
                .to_str()
                .ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?,
            "-o",
            target_path
                .as_ref()
                .to_str()
                .ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?,
        ],
        PopenConfig {
            stdin: Redirection::Pipe,
            stdout: Redirection::Pipe,
            stderr: Redirection::Pipe,
            ..Default::default()
        },
    )?;
    let exit_status = handler.wait()?;
    if !exit_status.success() {
        error!(
            LOGGER,
            "Failed to compile program {}",
            source_path
                .as_ref()
                .to_str()
                .ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?
        );
        bail!(ErrorKind::CompileError(format!("{:?}", exit_status)));
    }
    info!(
        LOGGER,
        "Finished compiling {}",
        source_path
            .as_ref()
            .to_str()
            .ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?
    );
    Ok(())
}

pub async fn read_data<T: AsRef<Path>>(data_path: T, index: usize) -> Result<String> {
    info!(LOGGER, "Start to read data {}", index);
    let mut in_path = data_path.as_ref().to_path_buf();
    in_path.push(index.to_string() + ".in");
    let mut result = String::new();
    let mut in_file = OpenOptions::new().read(true).open(&in_path)?;
    in_file.read_to_string(&mut result)?;
    info!(LOGGER, "Finished reading data {}", index);
    Ok(result)
}


pub async fn read_answer<T: AsRef<Path>>(data_path: T, index: usize) -> Result<String> {
    info!(LOGGER, "Start to read answer {}", index);
    let mut in_path = data_path.as_ref().to_path_buf();
    in_path.push(index.to_string() + ".ans");
    let mut result = String::new();
    let mut in_file = OpenOptions::new().read(true).open(&in_path)?;
    in_file.read_to_string(&mut result)?;
    info!(LOGGER, "Finished reading answer {}", index);
    Ok(result)
}


pub async fn read_output<T: AsRef<Path>>(data_path: T, suffix: Option<&str>, index: usize) -> Result<String> {
    let mut in_path = data_path.as_ref().to_path_buf();
    match suffix {
        Some(s) => {
            in_path.push(index.to_string() + s + ".out");
        },
        None => {
            in_path.push(index.to_string() + ".out");
        }
    }
    info!(LOGGER, "Start to read output {}", in_path.to_str().ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?);
    let mut result = String::new();
    let mut in_file = OpenOptions::new().read(true).open(&in_path)?;
    in_file.read_to_string(&mut result)?;
    info!(LOGGER, "Finished reading output {}", in_path.to_str().ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?);
    Ok(result)
}


pub async fn generate_data<E: AsRef<Path>>(data_path: E, index: usize) -> Result<Vec<u8>> {
    thread::sleep(Duration::from_millis(
        rand::thread_rng().gen::<u64>() % 500 + 1,
    ));
    let mut in_path = data_path.as_ref().to_path_buf();
    in_path.push(index.to_string() + ".in");
    info!(LOGGER, "Start to generate data {}", index);
    let mut seed_path = data_path.as_ref().to_path_buf();
    seed_path.push(index.to_string() + ".seed");
    let in_file = OpenOptions::new()
        .create(true)
        .write(true)
        .read(true)
        .truncate(true)
        .open(&in_path)?;
    let mut seed_file = OpenOptions::new()
        .create(true)
        .write(true)
        .read(true)
        .truncate(true)
        .open(&seed_path)?;
    write!(
        seed_file,
        "{}",
        rand::thread_rng().gen::<u32>() % 1000000000 + 1
    )?;
    seed_file.sync_all()?;
    let mut handler = Popen::create(
        &["data.exe"],
        PopenConfig {
            stdin: Redirection::File(seed_file),
            stdout: Redirection::File(in_file),
            stderr: Redirection::Pipe,
            ..Default::default()
        },
    )?;
    let exit_status = handler.wait()?;
    if !exit_status.success() {
        error!(LOGGER, "Failed to generate data index {}", index);
        bail!(ErrorKind::GenerateDataError(format!("{:?}", exit_status)));
    }
    let mut result = vec![];
    let mut in_file = OpenOptions::new().read(true).open(&in_path)?;
    in_file.read_to_end(&mut result)?;
    info!(LOGGER, "Finished generating data {}", index);
    Ok(result)
}

pub async fn spawn_and_save<T: AsRef<Path>, E: AsRef<Path>>(
    program_path: T,
    data_path: E,
    input_data: Vec<u8>,
    suffix: Option<&str>,
    index: usize,
) -> Result<()> {
    info!(
        LOGGER,
        "Start to spawn program {} with data index {}",
        program_path
            .as_ref()
            .to_str()
            .ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?,
        index
    );
    let mut out_path = data_path.as_ref().to_path_buf();
    match suffix {
        Some(s) => {
            out_path.push(index.to_string() + s + ".out");
        }
        None => {
            out_path.push(index.to_string() + ".out");
        }
    }
    let out_file = OpenOptions::new()
        .create(true)
        .write(true)
        .read(true)
        .truncate(true)
        .open(&out_path)?;
    let mut handler = Popen::create(
        &[program_path
            .as_ref()
            .to_str()
            .ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?],
        PopenConfig {
            stdin: Redirection::Pipe,
            stdout: Redirection::File(out_file),
            stderr: Redirection::Pipe,
            ..Default::default()
        },
    )?;
    handler.communicate_bytes(Some(&input_data))?;
    let exit_status = handler.wait()?;
    if !exit_status.success() {
        error!(
            LOGGER,
            "Failed to spawn program {} with data index {}",
            program_path
                .as_ref()
                .to_str()
                .ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?,
            index
        );
        bail!(ErrorKind::ProgramError(format!("{:?}", exit_status)));
    }
    info!(
        LOGGER,
        "Finished spawning program {} with data index {}",
        program_path
            .as_ref()
            .to_str()
            .ok_or::<Error>(ErrorKind::InvaildPathStrConvert.into())?,
        index
    );
    Ok(())
}

pub async fn compare_output(expected: String, actual: String) -> Result<()> {
    if !(expected == actual) {
        STAT.lock().await.WA += 1;
        error!(LOGGER, "Found difference!");
        return Err("DifferenceFound".into());
    }
    STAT.lock().await.AC += 1;
    Ok(())
}
